#include "simodo/lsp-server/ServerContext.h"
#include "simodo/lsp-server/DocumentContext.h"
#include "TaskDistribute.h"
#include "TaskAnalyze.h"

#include "simodo/LibVersion.h"

#include "simodo/variable/json/Serialization.h"
#include "simodo/inout/convert/functions.h"

#include <thread>
#include <iostream>
#include <chrono>
#include <iomanip>
#include <filesystem>

namespace simodo::lsp
{

inline const std::string content_length_const = "Content-Length: ";

namespace 
{
    std::pair<int, std::string> readHeader(std::istream & is)
    {
        /// @todo Необходимо заменить на работу с boost::asio, чтобы не было клинча в ожиданиях.
        /// Это когда клиент ждёт, когда сервер завершиться после нотификации о завершении, а 
        /// сервер в ожидании очередной команды. Или придумать что-то ещё.
        std::string header;
        std::string line;
        int         content_size = 0;

        while(std::getline(is,line) && !line.empty()) {
            if (line.find(content_length_const) == 0)
                content_size = std::stol(line.substr(content_length_const.size()));
            header += line + "\n";
        }

        return {content_size, header};
    }

    std::string readContent(std::istream & is, int content_length)
    {
        std::string content;

        for(int i=0; i < content_length && is.good(); ++i)
            content += is.get();

        return content;
    }

    bool verifyJson(const std::string & str)
    {
        return !str.empty() && str[0] == '{' && str[str.size()-1] == '}';
    }

    bool verifyExit(const std::string & json_str)
    {
        const std::string exit_request_string = "\"method\":\"exit\"";

        if (json_str.length() > 4*exit_request_string.length())
            return false;

        return json_str.find(exit_request_string) != std::string::npos;
    }
}

ServerContext::ServerContext(std::u16string server_name,
                             lsp::ServerCapabilities server_capabilities,
                             lsp::SimodoCapabilities simodo_capabilities,
                             DocumentOperationFactory_interface & document_operation_factory,
                             inout::Logger_interface & log, 
                             std::istream & is, 
                             std::ostream & os, 
                             bool save_mode)
    : _server_name(server_name)
    , _server_capabilities(server_capabilities)
    , _simodo_capabilities(simodo_capabilities)
    , _document_operation_factory(document_operation_factory)
    , _log(log)
    , _is(is)
    , _os(os)
    , _queue_for_sending()
    , _tp(save_mode ? 0 : 4)
    , _response_thread(&ServerContext::sendResponse_loop, this)
    , _m()
    , _save_mode(save_mode)
{
}

ServerContext::~ServerContext()
{
    _done = true;
    _tp.stop();
    _response_thread.join();
}

bool ServerContext::openDocument(const simodo::variable::Object & doc_params)
{
    const variable::Value & textDocument_value = doc_params.find(u"textDocument");
    if (textDocument_value.type() != variable::ValueType::Object)
        return false;

    const variable::Value & uri_value = textDocument_value.getObject()->find(u"uri");
    if (uri_value.type() != variable::ValueType::String)
        return false;

    const variable::Value & languageId_value = textDocument_value.getObject()->find(u"languageId");
    if (languageId_value.type() != variable::ValueType::String)
        return false;

    std::string         uri         = inout::toU8(uri_value.getString());
    std::string         languageId  = inout::toU8(languageId_value.getString());
    std::unique_lock    documents_locker(_documents_mutex);
    auto                it          = _documents.find(uri);

    if (it != _documents.end()) {
        if (it->second->is_opened())
            return false;

        if (!it->second->open(*textDocument_value.getObject()))
            return false;
    }
    else {
        std::unique_ptr<DocumentContext> doc 
                        = std::make_unique<DocumentContext>(
                                            *this,
                                            languageId,
                                            *textDocument_value.getObject());

        if (!doc->valid())
            return false;

        auto [it_doc,ok] = _documents.insert({uri,std::move(doc)});
        it = it_doc;
    }

    documents_locker.unlock();

    /// @attention Попавшие в контейнер документы нельзя оттуда удалять!
    tp().submit(new TaskAnalyze(*it->second));

    return true;
}

bool ServerContext::changeDocument(const simodo::variable::Object & doc_params)
{
    const variable::Value & textDocument_value = doc_params.find(u"textDocument");
    if (textDocument_value.type() != variable::ValueType::Object) {
        log().error("ServerContext::changeDocument: Wrong params structure #1");
        return false;
    }

    const variable::Value & uri_value = textDocument_value.getObject()->find(u"uri");
    if (uri_value.type() != variable::ValueType::String) {
        log().error("ServerContext::changeDocument: Wrong params structure #2");
        return false;
    }

    std::string     uri   = inout::toU8(uri_value.getString());
    std::lock_guard documents_locker(_documents_mutex);
    auto            it    = _documents.find(uri);
    if (it == _documents.end()) {
        log().error("ServerContext::changeDocument: Document '" + uri + "' not found");
        return false;
    }

    if (!it->second->change(doc_params)) {
        log().error("ServerContext::changeDocument: doc->change return false");
        return false;
    }

    /// @attention Попавшие в контейнер документы нельзя оттуда удалять!
    log().debug("ServerContext::changeDocument: start analyze for '" + it->second->file_name() + "'");
    tp().submit(new TaskAnalyze(*it->second));

    for(auto & [doc_uri,p_doc] : _documents)
        if (doc_uri != uri && p_doc->is_opened() && p_doc->checkDependency(uri)) {
            log().debug("ServerContext::changeDocument: start analyze for '" + p_doc->file_name() + "'");
            tp().submit(new TaskAnalyze(*p_doc));
        }

    return true;
}

bool ServerContext::closeDocument(const std::string & uri)
{
    std::lock_guard locker(_documents_mutex);
    auto it = _documents.find(uri);

    if (it == _documents.end())
        return false;

    return it->second->close();
}

DocumentContext * ServerContext::findDocument(const std::string & uri) 
{
    std::lock_guard locker(_documents_mutex);

    auto it = _documents.find(uri);
    
    return it != _documents.end() ? it->second.get() : nullptr;
}

const DocumentContext * ServerContext::findDocument(const std::string & uri) const
{
    std::lock_guard locker(_documents_mutex);

    auto it = _documents.find(uri);
    
    return it != _documents.end() ? it->second.get() : nullptr;
}

bool ServerContext::findAndCopy(const std::string & uri, std::u16string & content)
{
    std::lock_guard locker(_documents_mutex);
    auto it = _documents.find(uri);

    if (it == _documents.end())
        return false;

    it->second->copyContent(content);
    return true;
}

variable::JsonRpc ServerContext::initialize(lsp::ClientParams params, int id)
{
    _client_params = params;

    using namespace variable;

    if (_client_params.rootPath.empty())
        _client_params.rootPath = std::filesystem::current_path().string();
    else {
        std::error_code ec;
        std::filesystem::path work_dir = _client_params.rootPath;
        std::filesystem::current_path(work_dir, ec);
        if (ec)
            /// @todo Скорректировать коды (и тексты) ошибок
            return JsonRpc(-1, u"Unable to set work dir '" + inout::toU16(_client_params.rootPath) + u"'. " 
                                + inout::toU16(ec.message()), id);
    }

    std::vector<Value>  triggerCharacters;
    for (std::u16string & s : _server_capabilities.completionProvider.triggerCharacters)
        triggerCharacters.push_back(s);

    std::vector<Value>  tokenTypes;
    for (std::u16string & s : _server_capabilities.semanticTokensProvider.legend.tokenTypes)
        tokenTypes.push_back(s);

    std::vector<Value>  tokenModifiers;
    for (std::u16string & s : _server_capabilities.semanticTokensProvider.legend.tokenModifiers)
        tokenModifiers.push_back(s);

    std::vector<Value>  simodoRunnerOptions;
    for (const SimodoRunnerOption & opt : _simodo_capabilities.runnerOptions)
        simodoRunnerOptions.push_back(Object {{
            {u"languageID", opt.languageID},
            {u"viewName",   opt.viewName}
        }});

    return JsonRpc(Value { Object {{
        {u"capabilities", Object {{
            // The position encoding the server picked from the encodings offered by the 
            // client via the client capability `general.positionEncodings`.
            {u"positionEncoding",       POSITION_ENCODING_UTF16},
            // Documents are synced by always sending the full content of the document.
            {u"textDocumentSync",       static_cast<int64_t>(TextDocumentSyncKind::Full)},
            // The server provides completion support.
            {u"completionProvider",     Object {{
                {u"triggerCharacters",      Array {triggerCharacters}},
                // The server provides support to resolve additional information for a completion item.
                {u"resolveProvider",        _server_capabilities.completionProvider.resolveProvider},
                // The server supports the following `CompletionItem` specific capabilities.
                {u"completionItem",         Object {{
                    // The server has support for completion item label details (see also 
                    // `CompletionItemLabelDetails`) when receiving a completion item in a
                    // resolve call.
                    {u"labelDetailsSupport",_server_capabilities.completionProvider.completionItem.labelDetailsSupport},
                }}},
            }}},
            // The server provides hover support.
            {u"hoverProvider",          _server_capabilities.hoverProvider},
            // The server provides go to declaration support.
            {u"declarationProvider",    _server_capabilities.declarationProvider},
            // The server provides goto definition support.
            {u"definitionProvider",     _server_capabilities.definitionProvider},
            // The server provides document symbol support.
            {u"documentSymbolProvider", _server_capabilities.documentSymbolProvider},
            // The server provides semantic tokens support.
            {u"semanticTokensProvider", Object {{
                {u"legend", Object {{
                    {u"tokenTypes",         Array {tokenTypes}},
                    {u"tokenModifiers",     Array {tokenModifiers}},
                }}},
                // Server supports providing semantic tokens for a full document.
                {u"full",   true},
            }}},
        }}},
        {u"simodo", Object {{
            {u"runnerProvider",         _simodo_capabilities.runnerProvider},
            {u"debugProvider",          _simodo_capabilities.debugProvider},
            {u"runnerOptions",          Array {simodoRunnerOptions}},
        }}},
        {u"serverInfo", Object {{
            {u"name",                   u"SIMODO/loom " + _server_name},
            {u"version",                inout::toU16(
                                                std::to_string(static_cast<int>(lib_version().major()))+"."+
                                                std::to_string(static_cast<int>(lib_version().minor())))},
        }}},
    }}}, id);
}

void ServerContext::initialized()
{
    _state = ServerState::Work;
}

bool ServerContext::shutdown()
{
    _state = ServerState::Shutdown;
    /// @todo Выполнить необходимые действия для сохранения состояния, если это нужно
    return true;
}

void ServerContext::exit()
{
    _done = true;
}

bool ServerContext::run()
{
    bool ok = true;

    while(!is_done()) {
        auto [content_length, header] = readHeader(_is);
        if (header.empty() || content_length <= 0) {
            /// @todo Скорректировать коды (и тексты) ошибок
            _queue_for_sending.push(simodo::variable::JsonRpc(-1, u"Wrong header structure", -1));
            _log.critical("Wrong header structure", header);
            ok = false;
            break;
        }
        {
            std::ostringstream so;
            so  << std::endl 
                << "RECEIVE FROM CLIENT " << std::endl
                << "'" << header << "'" << std::endl;
            _log.debug(so.str());
        }

        std::string content = readContent(_is, content_length);
        if (content.empty()) {
            /// @todo Скорректировать коды (и тексты) ошибок
            _queue_for_sending.push(simodo::variable::JsonRpc(-1, u"Wrong content length", -1));
            _log.critical("Wrong content structure", content);
            ok = false;
            break;
        }
        {
            std::ostringstream so;
            so << content << std::endl;
            _log.debug("Content of JSON-RPC", so.str());
        }

        if (!verifyJson(content)) {
            /// @todo Скорректировать коды (и тексты) ошибок
            _queue_for_sending.push(simodo::variable::JsonRpc(-1, u"Wrong JSON structure", -1));
            _log.critical("Wrong JSON structure", content);
            ok = false;
            break;
        }

        /// @attention Завершающую нотификацию сделал сразу при входном анализе запросов клиента, 
        /// чтобы корректно завершить работу
        if (verifyExit(content)) {
            _log.info("EXIT");
            exit();
            break;
        }

        tp().submit(new TaskDistribute(*this, content));

        if (save_mode())
            std::this_thread::sleep_for(std::chrono::milliseconds(500));
    }
    
    /// @todo Дожидаться отправки всех сообщений? Вроде бы не нужно, т.к. протокол
    /// исключает такую необходимость.
    return ok;
}

void ServerContext::sendResponse_loop()
{
    /// @attention Очереди входящих запросов, как и исходящих ответов 
    /// при завершение работы могут оставаться не пустыми.
    while(!_done) {
        variable::JsonRpc response;
        if (_queue_for_sending.pop_or_timeout(response, 100)) {
            std::string json_string = variable::toJson(response.value(), true, true);
            std::ostringstream log_str;
            log_str << std::endl 
                    << "SEND TO CLIENT " << std::endl
                    << content_length_const << json_string.length() << std::endl << std::endl 
                    << json_string;
            _log.debug(log_str.str());
            _os     << content_length_const << json_string.length() << std::endl << std::endl 
                    << json_string;
            _os.flush();
        }
    }
}

}